Skip to content

zlib: add ZIP archive support to zlib (ZipFile,ZipBuffer,ZipEntry) - #64339

Open
pipobscure wants to merge 2 commits into
nodejs:mainfrom
pipobscure:ziparchives
Open

zlib: add ZIP archive support to zlib (ZipFile,ZipBuffer,ZipEntry)#64339
pipobscure wants to merge 2 commits into
nodejs:mainfrom
pipobscure:ziparchives

Conversation

@pipobscure

@pipobscure pipobscure commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Add ZIP archive support to the node:zlib module through three classes
and a set of helpers:

  • ZipEntry: a single archive member, with buffered reads (content()),
    bounded-memory streaming reads (contentIterator()), and
    create()/createStream() for building members.
  • ZipFile: random access to an archive backed by a file descriptor,
    reading members lazily without retaining their content and writing
    new members in place; opened with open()/openSync().
  • ZipBuffer: a zero-copy, in-memory view over an archive already held
    in a Buffer.

createZipArchive() serializes a sequence of entries into an archive
byte stream, and setMaxZipContentSize() bounds the default in-memory
decompression size. Every operation has both an asynchronous and a
synchronous form.

P.S.: my CLA should be on file and I wrote this myself so COO is declared

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Jul 7, 2026
@pipobscure pipobscure changed the title zlib, vfs: add ZIP archive read/write support and an archive vfs provider zlib, vfs: add ZIP archive support and an archive vfs provider Jul 7, 2026
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.24856% with 73 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.31%. Comparing base (396aad0) to head (fb49aca).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/zip/file.js 96.41% 27 Missing and 1 partial ⚠️
lib/internal/zip/entry.js 98.44% 13 Missing and 2 partials ⚠️
lib/internal/zip/headers.js 97.26% 14 Missing ⚠️
lib/internal/zip/fs-util.js 92.00% 6 Missing and 6 partials ⚠️
lib/internal/zip/archive.js 99.37% 2 Missing ⚠️
lib/internal/zip/compression.js 99.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64339      +/-   ##
==========================================
+ Coverage   90.17%   90.31%   +0.13%     
==========================================
  Files         746      760      +14     
  Lines      242776   246928    +4152     
  Branches    45741    46578     +837     
==========================================
+ Hits       218929   223003    +4074     
- Misses      15347    15411      +64     
- Partials     8500     8514      +14     
Files with missing lines Coverage Δ
lib/internal/errors.js 97.91% <100.00%> (+<0.01%) ⬆️
lib/internal/zip.js 100.00% <100.00%> (ø)
lib/internal/zip/binary.js 100.00% <100.00%> (ø)
lib/internal/zip/buffer.js 100.00% <100.00%> (ø)
lib/internal/zip/constants.js 100.00% <100.00%> (ø)
lib/internal/zip/content-size.js 100.00% <100.00%> (ø)
lib/internal/zip/dos.js 100.00% <100.00%> (ø)
lib/internal/zip/extra-fields.js 100.00% <100.00%> (ø)
lib/internal/zip/header-builders.js 100.00% <100.00%> (ø)
lib/zlib.js 98.20% <100.00%> (+0.12%) ⬆️
... and 6 more

... and 40 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pipobscure

This comment was marked as outdated.

@pipobscure
pipobscure force-pushed the ziparchives branch 3 times, most recently from 0f2b879 to 4258e94 Compare July 7, 2026 19:20
pipobscure added a commit to pipobscure/node that referenced this pull request Jul 7, 2026
Codecov flagged low patch coverage on lib/internal/zip.js and
lib/internal/vfs/providers/archive.js in nodejs#64339. Add tests exercising
Zip64 extra-field parsing, DOS date/time edge cases, streaming-entry
state guards, decodeMemberStream/decodeMemberSync's duplicated error
branches, the ZipBuffer/ZipFile iteration protocols, several on-disk
ZipFile error paths, and ArchiveFileHandle's direct read/write/stat/
truncate surface plus a handful of provider-level error branches the
existing tests didn't reach.
@pipobscure
pipobscure force-pushed the ziparchives branch 2 times, most recently from 2a56a15 to 27ab6b4 Compare July 7, 2026 21:54
@bakkot

bakkot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

See also #45651

Comment thread doc/api/vfs.md Outdated
@pipobscure

pipobscure commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

See also #45651

Thanks @bakkot !!!

I think the time has come for it on the one hand, and on the other I added some „motivation“ links earlier. Here some more detail:

Based on this, we can modify the loader to directly load from an archive. If we do that, we get application bundles. pipobscure#5 & pipobscure#6

Which can then in turn be used to easily create application bundles: https://github.com/pipobscure/experimental-sea

So the world had changed enough that it‘s worth proposing again.

@JakobJingleheimer
JakobJingleheimer self-requested a review July 8, 2026 08:40
@pipobscure
pipobscure force-pushed the ziparchives branch 2 times, most recently from f157686 to 233f865 Compare July 8, 2026 11:29
@mcollina

mcollina commented Jul 8, 2026

Copy link
Copy Markdown
Member

I like this a lot. It's likely better to split this into 2 PRs, one for Zip support and one for VFS-Zip, so the Zip support could theoretically be backportable on its own.

Comment thread doc/api/zlib.md
@pipobscure

This comment was marked as outdated.

@pipobscure

pipobscure commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

As per @mcollina I split this into two PRs.

I have the vfs-provider ready to go as follow up one (as it depends on this being merged)

I also made sure that the streaming side of things was actually as clean as I intended, and added a few more tests.

@pipobscure pipobscure changed the title zlib, vfs: add ZIP archive support and an archive vfs provider zlib, vfs: add ZIP archive support to zlib Jul 8, 2026
Comment thread test/parallel/test-zlib-zip-vfs.js Outdated
@pipobscure pipobscure changed the title zlib, vfs: add ZIP archive support to zlib zlib: add ZIP archive support to zlib (ZipFile,ZipBuffer,ZipEntry) Jul 8, 2026
@bakkot

bakkot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Might be worth borrowing some tests from other projects: e.g. python, go, info-zip. There's a lot of edge cases with zip files. (Python's tests are larger than this whole PR put together.)

Not all directly applicable because this doesn't include the ability to extract to disk, fortunately.

@trivikr trivikr added the zlib Issues and PRs related to the zlib subsystem. label Jul 9, 2026
@pipobscure

Copy link
Copy Markdown
Contributor Author

Might be worth borrowing some tests from other projects

I've added a bunch more tests and compared to what go/python/info-zip do. (info-zip is cli exercise, so it's not entirely clear what gets tested).

I also gave zip.js an once over and concluded that it was too large a file (it originated from separate files that I've had for ages combined into one).

So I split it back out so it would be easier to review. And gave that another look.

@pipobscure

Copy link
Copy Markdown
Contributor Author

Rebased on latest main to resolve the conflict

@pipobscure

Copy link
Copy Markdown
Contributor Author

The failing mac tests seem to be entirely unrelated

Failed tests:
out/Release/node /Users/runner/work/node/node/node/test/parallel/test-debugger-extract-function-name.mjs
out/Release/node --test-reporter=./test/common/test-error-reporter.js --test-reporter-destination=stdout /Users/runner/work/node/node/node/test/parallel/test-repl-user-error-handler.js

can someone verify my deduction please.

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Jul 24, 2026
@pipobscure

This comment was marked as outdated.

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina mcollina added request-ci Add this label to start a Jenkins CI on a PR. and removed request-ci Add this label to start a Jenkins CI on a PR. labels Jul 29, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Jul 29, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@pipobscure

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@mcollina

mcollina commented Jul 30, 2026

Copy link
Copy Markdown
Member

Everything is green, CI is ok, we are missing another @nodejs/tsc approval as this a large PR.

Comment thread doc/api/zlib.md Outdated

@marco-ippolito marco-ippolito left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@targos targos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am reviewing. You can dismiss my request for changes if I don't answer by tomorrow.

@panva panva added the experimental Issues and PRs related to experimental features. label Jul 30, 2026
panva
panva previously requested changes Jul 30, 2026

@panva panva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { createGzip } from 'node:zlib'

This stable import emits ZIP experimental warning because of how our ESM facade evaluates the getters.

Add ZIP archive support to the node:zlib module through three classes
and a set of helpers:

- ZipEntry: a single archive member, with buffered reads (content()),
  bounded-memory streaming reads (contentIterator()), and
  create()/createStream() for building members.
- ZipFile: random access to an archive backed by a file descriptor,
  reading members lazily without retaining their content and writing
  new members in place; opened with open()/openSync().
- ZipBuffer: a zero-copy, in-memory view over an archive already held
  in a Buffer.

createZipArchive() serializes a sequence of entries into an archive
byte stream, and setMaxZipContentSize() bounds the default in-memory
decompression size. Every operation has both an asynchronous and a
synchronous form.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
@pipobscure

pipobscure commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

I rebased on main. Then I addressed the docs issue from @marco-ippolito and the import issue by @panva . I also go a security review from @mcollina and dealt with the issues that raised.

Here are the security-review issues we addressed:

Numbered findings

  • Finding 1 (High): After ZipFile.close(), retained entries/operations fell through to a raw fd that the OS could reuse, enabling cross-file read/write/close.
  • Finding 2 (High): add() awaited compression before queuing the mutation, so a concurrent close()/closeSync() could tear down the fd mid-add.
  • Finding 3 (High): zipFiles() re-opened files by path (and never fstat-checked type), allowing a symlink-swap TOCTOU and accepting FIFOs/devices.
  • Finding 4 (Medium): The open-time overlap check used a 30-byte lower bound instead of the exact local-header length, so a quoted-overlap archive passed ZipFile.open though ZipBuffer rejected it.

Additional concerns

  • A (in-place corruption): A failed central-directory rewrite can leave the on-disk archive corrupt and diverged from memory — documented as non-atomic (recovery preserved).
  • B (streaming ceiling): contentIterator()/stream() ignored the default decompression ceiling, allowing a zip bomb through the streaming API.
  • C (EOCD/ZIP64 mismatch): A ZIP64 record silently overrode contradictory classic-EOCD values with no consistency check, a parser-differential risk.
  • D (provisional streamed bytes): Streamed chunks are yielded before CRC verification runs at stream end — inherent to streaming, so documented.
  • E (FIFOs/devices): Special files were accepted as stream sources — resolved by the same fstat/type-check as Finding 3.

@panva

panva commented Jul 30, 2026

Copy link
Copy Markdown
Member

I rebased on main.

So we can't easily see and track the requested diff now. Unfortunate as otherwise we could review increments instead.

@panva
panva dismissed their stale review July 30, 2026 18:03

stale, need to start over now.

@pipobscure

Copy link
Copy Markdown
Contributor Author

I rebased on main.

So we can't easily see and track the requested diff now. Unfortunate as otherwise we could review increments instead.

Apologies. I thought the “requested modus operandi “ was to keep it to a single commit. If that’s wrong then I’ll change the approach for the future.

@panva

panva commented Jul 30, 2026

Copy link
Copy Markdown
Member

Apologies. I thought the “requested modus operandi “ was to keep it to a single commit. If that’s wrong then I’ll change the approach for the future.

No worries. FYI the commit-queue can land fixup commits (autosquash), or we can apply a label to squash, or if the PR has multiple things that can/should be backported independently (e.g. part is semver-major, part isn't) we can also land with rebase.

contentIterator() (and ZipFile.stream()) must not apply the default
getMaxZipContentSize() limit the way content()/contentSync() do. That
default guards a single large buffer allocation, which the streaming path
never makes: streaming is the bounded-memory way to read arbitrarily
large members. Applying the ceiling there rejected legitimate reads of
members larger than it - including the >4 GiB streaming case in
test/pummel/test-zlib-zip-slow.js.

Output stays bounded per chunk to the declared uncompressed size, and a
caller that wants an explicit cap can still pass options.maxSize.
@pipobscure

Copy link
Copy Markdown
Contributor Author

Loving tests right now. It caught me breaking streaming when doing the sec-fixes earlier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experimental Issues and PRs related to experimental features. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version. zlib Issues and PRs related to the zlib subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.